<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Interactive 3D Fish Flocking Simulation</title>
    <style>
        body { margin: 0; overflow: hidden; background-color: #f0f0f0; }
        canvas { display: block; }
        #fishCountUI {
            position: absolute;
            top: 10px;
            left: 10px;
            color: white;
            background-color: rgba(0,0,0,0.7);
            padding: 8px 12px;
            border-radius: 5px;
            font-family: Arial, sans-serif;
            font-size: 16px;
            z-index: 100;
        }
    </style>
</head>
<body>
    <div id="fishCountUI">Fish Count: 0</div>

    <script type="importmap">
        {
            "imports": {
                "three": "https://unpkg.com/three@0.160.0/build/three.module.js",
                "three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
            }
        }
    </script>

    <script type="module">
        import * as THREE from 'three';
        import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

        let scene, camera, renderer, controls;
        const fishes = [];
        const grassBlades = [];
        const initialNumFish = 10; // Default number of fish

        // Tank dimensions (inner dimensions for fish movement)
        const tankSize = { width: 2, height: 1, depth: 1 };
        const tankWallThickness = 0.05;

        // Flocking parameters
        const baseMaxSpeed = 0.01;
        const baseMaxForce = 0.0005; 
        
        const separationDistance = 0.2;
        const alignmentDistance = 0.5;
        const cohesionDistance = 0.5;

        const separationWeight = 1.5;
        const alignmentWeight = 1.0;
        const cohesionWeight = 1.0;

        // Panic state parameters
        let isPanicking = false;
        let panicEndTime = 0;
        const panicDuration = 4000; // 4 seconds in milliseconds
        const panicMaxSpeedFactor = 2.5;
        const panicMaxForceFactor = 2.5;

        const raycaster = new THREE.Raycaster();
        const mouse = new THREE.Vector2();
        let fishCountElement;


        class Boid {
            constructor() {
                const geometry = new THREE.ConeGeometry(0.03, 0.1, 8);
                geometry.rotateX(Math.PI / 2); 
                this.originalColor = new THREE.Color(Math.random() * 0.5 + 0.5, Math.random() * 0.5 + 0.5, Math.random() * 0.5 + 0.5);
                const material = new THREE.MeshStandardMaterial({ 
                    color: this.originalColor.clone(),
                    roughness: 0.5,
                    metalness: 0.2
                });
                this.mesh = new THREE.Mesh(geometry, material);
                
                // Position within the water volume of the tank
                const waterCenterX = 0;
                const waterCenterY = tankWallThickness + tankSize.height / 2;
                const waterCenterZ = 0;

                this.mesh.position.set(
                    waterCenterX + (Math.random() - 0.5) * (tankSize.width * 0.8),
                    waterCenterY + (Math.random() - 0.5) * (tankSize.height * 0.8),
                    waterCenterZ + (Math.random() - 0.5) * (tankSize.depth * 0.8)
                );

                this.velocity = new THREE.Vector3(
                    (Math.random() - 0.5) * baseMaxSpeed,
                    (Math.random() - 0.5) * baseMaxSpeed,
                    (Math.random() - 0.5) * baseMaxSpeed
                );
                this.acceleration = new THREE.Vector3();

                this.currentMaxSpeed = baseMaxSpeed;
                this.currentMaxForce = baseMaxForce;
                this.isInPanicState = false; // Individual boid panic state
            }

            applyForce(force) {
                this.acceleration.add(force);
            }

            // --- Flocking Rules ---
            separate(boids) {
                let steer = new THREE.Vector3();
                let count = 0;
                for (let other of boids) {
                    if (other === this) continue;
                    let d = this.mesh.position.distanceTo(other.mesh.position);
                    if (d > 0 && d < separationDistance) {
                        let diff = new THREE.Vector3().subVectors(this.mesh.position, other.mesh.position);
                        diff.normalize();
                        diff.divideScalar(d); 
                        steer.add(diff);
                        count++;
                    }
                }
                if (count > 0) steer.divideScalar(count);
                if (steer.lengthSq() > 0) {
                    steer.normalize();
                    steer.multiplyScalar(this.currentMaxSpeed);
                    steer.sub(this.velocity);
                    steer.clampLength(0, this.currentMaxForce);
                }
                return steer;
            }

            align(boids) {
                let steer = new THREE.Vector3();
                let count = 0;
                for (let other of boids) {
                    if (other === this) continue;
                    let d = this.mesh.position.distanceTo(other.mesh.position);
                    if (d > 0 && d < alignmentDistance) {
                        steer.add(other.velocity);
                        count++;
                    }
                }
                if (count > 0) {
                    steer.divideScalar(count);
                    steer.normalize();
                    steer.multiplyScalar(this.currentMaxSpeed);
                    steer.sub(this.velocity);
                    steer.clampLength(0, this.currentMaxForce);
                }
                return steer;
            }

            cohesion(boids) {
                let steer = new THREE.Vector3();
                let count = 0;
                for (let other of boids) {
                    if (other === this) continue;
                    let d = this.mesh.position.distanceTo(other.mesh.position);
                    if (d > 0 && d < cohesionDistance) {
                        steer.add(other.mesh.position);
                        count++;
                    }
                }
                if (count > 0) {
                    steer.divideScalar(count);
                    let desired = new THREE.Vector3().subVectors(steer, this.mesh.position);
                    desired.normalize();
                    desired.multiplyScalar(this.currentMaxSpeed);
                    desired.sub(this.velocity);
                    desired.clampLength(0, this.currentMaxForce);
                    return desired;
                }
                return steer; 
            }
            
            boundaries() {
                const steer = new THREE.Vector3();
                const buffer = 0.05; // Buffer from the glass
                const minX = -tankSize.width / 2 + buffer;
                const maxX =  tankSize.width / 2 - buffer;
                const minY =  tankWallThickness + buffer;
                const maxY =  tankWallThickness + tankSize.height - buffer;
                const minZ = -tankSize.depth / 2 + buffer;
                const maxZ =  tankSize.depth / 2 - buffer;

                if (this.mesh.position.x > maxX) steer.x = -this.currentMaxSpeed;
                else if (this.mesh.position.x < minX) steer.x = this.currentMaxSpeed;

                if (this.mesh.position.y > maxY) steer.y = -this.currentMaxSpeed;
                else if (this.mesh.position.y < minY) steer.y = this.currentMaxSpeed;
                
                if (this.mesh.position.z > maxZ) steer.z = -this.currentMaxSpeed;
                else if (this.mesh.position.z < minZ) steer.z = this.currentMaxSpeed;

                if (steer.lengthSq() > 0) {
                    steer.normalize();
                    steer.multiplyScalar(this.currentMaxSpeed); // Use currentMaxSpeed for turning
                    steer.sub(this.velocity);
                    steer.clampLength(0, this.currentMaxForce * 2); // Stronger force for boundaries
                    this.applyForce(steer);
                }
            }

            enterPanic() {
                this.isInPanicState = true;
                this.currentMaxSpeed = baseMaxSpeed * panicMaxSpeedFactor;
                this.currentMaxForce = baseMaxForce * panicMaxForceFactor;
                // this.mesh.material.color.setHex(0xff0000); // Optional: make fish red during panic
            }

            exitPanic() {
                this.isInPanicState = false;
                this.currentMaxSpeed = baseMaxSpeed;
                this.currentMaxForce = baseMaxForce;
                // this.mesh.material.color.copy(this.originalColor); // Revert color
            }

            flock(boids) {
                let sep = this.separate(boids);
                let ali = this.align(boids);
                let coh = this.cohesion(boids);

                if (this.isInPanicState && isPanicking) { // Check global and individual state
                    sep.multiplyScalar(separationWeight * 1.8); // Stronger separation
                    ali.multiplyScalar(alignmentWeight * 0.2);   // Weaker alignment
                    coh.multiplyScalar(cohesionWeight * 0.2);   // Weaker cohesion

                    const randomPush = new THREE.Vector3(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5);
                    randomPush.normalize().multiplyScalar(this.currentMaxForce * 0.6);
                    this.applyForce(randomPush);
                } else {
                    sep.multiplyScalar(separationWeight);
                    ali.multiplyScalar(alignmentWeight);
                    coh.multiplyScalar(cohesionWeight);
                }

                this.applyForce(sep);
                this.applyForce(ali);
                this.applyForce(coh);
                this.boundaries();
            }

            update() {
                this.velocity.add(this.acceleration);
                this.velocity.clampLength(0, this.currentMaxSpeed);
                this.mesh.position.add(this.velocity);
                this.acceleration.multiplyScalar(0); 

                if (this.velocity.lengthSq() > 0.00001) { 
                    const targetPosition = this.mesh.position.clone().add(this.velocity);
                    this.mesh.lookAt(targetPosition);
                }
            }
        }

        class GrassBlade {
            constructor(height, baseWidth, tipWidth, segments) {
                this.height = height;
                this.baseWidth = baseWidth;
                this.tipWidth = tipWidth;
                this.segments = segments;
                this.geometry = this.createGrassGeometry();
                this.material = new THREE.MeshStandardMaterial({
                    color: 0x33aa33, 
                    roughness: 0.8,
                    metalness: 0.1,
                    side: THREE.DoubleSide 
                });
                this.mesh = new THREE.Mesh(this.geometry, this.material);
                this.mesh.castShadow = true;
                this.originalPositions = this.geometry.attributes.position.array.slice();
                this.swayFrequency = Math.random() * 0.5 + 0.3; 
                this.swayAmplitude = Math.random() * 0.02 + 0.01;
                this.phaseOffset = Math.random() * Math.PI * 2;
            }
            createGrassGeometry() {
                const geometry = new THREE.BufferGeometry();
                const vertices = []; const indices = []; const uvs = [];
                const segmentHeight = this.height / this.segments;
                for (let i = 0; i <= this.segments; i++) {
                    const y = i * segmentHeight;
                    const t = i / this.segments; 
                    const currentWidth = this.baseWidth * (1 - t) + this.tipWidth * t;
                    vertices.push(-currentWidth / 2, y, 0); vertices.push(currentWidth / 2, y, 0);
                    uvs.push(0, t); uvs.push(1, t);
                    if (i < this.segments) {
                        const i0 = i * 2; const i1 = i0 + 1; const i2 = i0 + 2; const i3 = i0 + 3;
                        indices.push(i0, i1, i2); indices.push(i1, i3, i2);
                    }
                }
                geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
                geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
                geometry.setIndex(indices);
                geometry.computeVertexNormals();
                return geometry;
            }
            update(time) {
                const positions = this.geometry.attributes.position;
                const original = this.originalPositions;
                for (let i = 0; i < positions.count; i++) {
                    const originalX = original[i * 3]; const originalY = original[i * 3 + 1];
                    const swayFactor = (originalY / this.height); 
                    const swayX = Math.sin(time * this.swayFrequency + this.phaseOffset + originalY * 0.5) * this.swayAmplitude * swayFactor;
                    const swayZ = Math.cos(time * this.swayFrequency * 0.7 + this.phaseOffset + originalY * 0.3) * this.swayAmplitude * swayFactor * 0.3;
                    positions.array[i * 3] = originalX + swayX;
                    positions.array[i * 3 + 2] = swayZ; 
                }
                positions.needsUpdate = true;
                this.geometry.computeVertexNormals(); 
            }
        }

        function init() {
            fishCountElement = document.getElementById('fishCountUI');

            scene = new THREE.Scene();
            scene.background = new THREE.Color(0x87ceeb); 

            camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
            camera.position.set(1.5, 1.5, 2.5);
            // Adjust camera target to be center of water volume
            camera.lookAt(0, tankWallThickness + tankSize.height / 2, 0);


            renderer = new THREE.WebGLRenderer({ antialias: true });
            renderer.setSize(window.innerWidth, window.innerHeight);
            renderer.setPixelRatio(window.devicePixelRatio);
            renderer.shadowMap.enabled = true;
            document.body.appendChild(renderer.domElement);

            controls = new OrbitControls(camera, renderer.domElement);
            // Adjust controls target
            controls.target.set(0, tankWallThickness + tankSize.height / 2, 0);
            controls.update();
            controls.enableDamping = true;

            const ambientLight = new THREE.AmbientLight(0xffffff, 0.8);
            scene.add(ambientLight);

            const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2);
            directionalLight.position.set(5, 10, 7.5);
            directionalLight.castShadow = true;
            directionalLight.shadow.mapSize.width = 1024;
            directionalLight.shadow.mapSize.height = 1024;
            scene.add(directionalLight);

            const tableGeometry = new THREE.BoxGeometry(3, 0.1, 2);
            const tableMaterial = new THREE.MeshStandardMaterial({ color: 0x8B4513, roughness: 0.8 }); 
            const table = new THREE.Mesh(tableGeometry, tableMaterial);
            table.position.y = -0.05; 
            table.receiveShadow = true;
            scene.add(table);

            const tankOuterWidth = tankSize.width + 2 * tankWallThickness;
            const tankOuterHeight = tankSize.height + 2 * tankWallThickness;
            const tankOuterDepth = tankSize.depth + 2 * tankWallThickness;

            const tankGeometry = new THREE.BoxGeometry(tankOuterWidth, tankOuterHeight, tankOuterDepth);
            const tankMaterial = new THREE.MeshStandardMaterial({
                color: 0xadd8e6, 
                transparent: true,
                opacity: 0.2,
                roughness: 0.1,
                metalness: 0.1
            });
            const fishTank = new THREE.Mesh(tankGeometry, tankMaterial);
            fishTank.position.y = tankOuterHeight / 2; // Tank base is on the table (y=0)
            scene.add(fishTank);
            
            const numPlants = 7;
            for (let i = 0; i < numPlants; i++) {
                const plantHeight = Math.random() * 0.4 + 0.3; 
                const baseWidth = Math.random() * 0.03 + 0.04; 
                const tipWidth = baseWidth * (Math.random() * 0.3 + 0.1); 
                const segments = 5; 
                const grass = new GrassBlade(plantHeight, baseWidth, tipWidth, segments);
                grass.mesh.position.set(
                    (Math.random() - 0.5) * (tankSize.width * 0.85), 
                    tankWallThickness, // On the bottom of the tank (inner bottom)
                    (Math.random() - 0.5) * (tankSize.depth * 0.85)
                );
                grass.mesh.rotation.y = Math.random() * Math.PI * 2; 
                grassBlades.push(grass);
                scene.add(grass.mesh);
            }

            createInitialFishSet();
            updateFishCountUI();

            window.addEventListener('resize', onWindowResize, false);
            window.addEventListener('mousedown', onMouseDown, false);
            window.addEventListener('contextmenu', onRightClick, false);
        }

        function createBoid() {
            const boid = new Boid();
            fishes.push(boid);
            scene.add(boid.mesh);
            boid.mesh.castShadow = true;
        }

        function createInitialFishSet() {
            for (let i = 0; i < initialNumFish; i++) {
                createBoid();
            }
        }

        function updateFishCountUI() {
            if (fishCountElement) {
                fishCountElement.textContent = `Fish Count: ${fishes.length}`;
            }
        }

        function onMouseDown(event) {
            if (event.button === 0) { // Left mouse button
                mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
                mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
                raycaster.setFromCamera(mouse, camera);

                const fishMeshes = fishes.map(f => f.mesh);
                const intersects = raycaster.intersectObjects(fishMeshes);

                if (intersects.length > 0) {
                    const intersectedMesh = intersects[0].object;
                    killFishByMesh(intersectedMesh);
                }
            }
        }

        function killFishByMesh(mesh) {
            const index = fishes.findIndex(boid => boid.mesh === mesh);
            if (index !== -1) {
                const boid = fishes[index];
                scene.remove(boid.mesh);
                boid.mesh.geometry.dispose();
                if (boid.mesh.material.dispose) boid.mesh.material.dispose(); // Dispose material if unique
                
                fishes.splice(index, 1);
                updateFishCountUI();

                if (fishes.length > 0) { // Only trigger panic if there are other fish
                    triggerGroupPanic();
                }
            }
        }
        
        function triggerGroupPanic() {
            isPanicking = true;
            panicEndTime = Date.now() + panicDuration;
            fishes.forEach(boid => boid.enterPanic());
        }

        function checkAndEndPanic() {
            if (isPanicking && Date.now() > panicEndTime) {
                isPanicking = false;
                fishes.forEach(boid => boid.exitPanic());
            }
        }

        function onRightClick(event) {
            event.preventDefault(); 

            // Clear existing fish
            while (fishes.length > 0) {
                const boid = fishes.pop(); // Removes last element and returns it
                scene.remove(boid.mesh);
                boid.mesh.geometry.dispose();
                if (boid.mesh.material.dispose) boid.mesh.material.dispose();
            }
            
            // Reset panic state immediately
            isPanicking = false; 

            // Create new fish
            createInitialFishSet();
            updateFishCountUI();
        }


        function onWindowResize() {
            camera.aspect = window.innerWidth / window.innerHeight;
            camera.updateProjectionMatrix();
            renderer.setSize(window.innerWidth, window.innerHeight);
        }

        const clock = new THREE.Clock(); 

        function animate() {
            requestAnimationFrame(animate);
            const elapsedTime = clock.getElapsedTime();

            checkAndEndPanic(); // Check if global panic state should end

            fishes.forEach(boid => {
                boid.flock(fishes);
            });
            
            fishes.forEach(boid => {
                boid.update();
            });

            grassBlades.forEach(blade => {
                blade.update(elapsedTime);
            });

            controls.update(); 
            renderer.render(scene, camera);
        }

        init();
        animate();

    </script>
</body>
</html>